package edu.hawaii.senin.iclouds.basics;

import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.math.random.RandomAdaptor;
import org.apache.commons.math.random.RandomGenerator;

/**
 * Container for species population. It provides iterator over the species.
 * 
 * @author Pavel Senin.
 * 
 */
public class PopulationWithBury {

  /**
   * Internal active species storage.
   */
  private TreeMap<String, Specie> surfaceStorage;

  /**
   * Internal buried species storage.
   */
  private TreeMap<String, Specie> buriedStorage;

  /**
   * Amount of active sites.
   */
  private Long sitesNum;

  /**
   * Random number generator wrapper for RanMT.
   */
  private RandomGenerator randomGenerator;

  /**
   * Random number generator handler.
   */
  private Random random;

  private boolean buryOff;

  /**
   * Creates new empty Population.
   * 
   * @param sitesNum specifies the number of active of sites on the surface.
   * 
   */
  public PopulationWithBury(Long sitesNum) {
    this.sitesNum = sitesNum;
    this.buryOff = false;
    this.surfaceStorage = new TreeMap<String, Specie>();
    this.buriedStorage = new TreeMap<String, Specie>();
    // init random generator with the same seed.
    this.randomGenerator = new RngPackGenerator();
    this.random = RandomAdaptor.createAdaptor(this.randomGenerator);
    random.setSeed(((Long) (System.currentTimeMillis() * 31)).intValue());
  }

  /**
   * Adds species to existed population.
   * 
   * @param specieName specie name.
   * @param justAdd boolean parameter to switch bury routine on/off.
   * @throws PopulationException if error encountered.
   * @return some text for debugging.
   */
  public String addSpecie(String specieName, boolean justAdd) throws PopulationException {

    StringBuffer res = new StringBuffer(1000);

    if (this.buryOff) {
      // logging
      res.append("adding " + specieName);
      // add the specie
      if (this.surfaceStorage.containsKey(specieName)) {
        this.surfaceStorage.get(specieName).incrementAccumulatorValue(1L);
      }
      else {
        Specie sp = null;
        if (specieName.length() > 1) {
          sp = new Molecule(specieName, 1L);
        }
        else {
          sp = new Atom(specieName, 1L);
        }
        this.surfaceStorage.put(specieName, sp);
      }

    }
    else {

      Long totalActiveSize = this.surfaceTotalSize();

      res.append("BURIED,limit " + this.sitesNum + ",active:" + totalActiveSize + ",buried:"
          + this.buriedTotalSize());

      if ((justAdd) && (this.sitesNum <= totalActiveSize)) {
        res.append(", got to JUST ADD " + specieName + " BUT MAX HIT!");
        justAdd = false;
      }

      if (justAdd) {
        // logging
        res.append(",adding " + specieName);
        // add the specie
        if (this.surfaceStorage.containsKey(specieName)) {
          this.surfaceStorage.get(specieName).incrementAccumulatorValue(1L);
        }
        else {
          Specie sp = null;
          if (specieName.length() > 1) {
            sp = new Molecule(specieName, 1L);
          }
          else {
            sp = new Atom(specieName, 1L);
          }
          this.surfaceStorage.put(specieName, sp);
        }

      }
      else {
        res.append(",adding " + specieName + " BURYING ");

        Double probability2Bury = totalActiveSize.doubleValue() / this.sitesNum.doubleValue();

        Double rndLandingSection = this.random.nextDouble();

        res.append("buryP " + probability2Bury + ",rnd " + rndLandingSection + " ");

        if (rndLandingSection < probability2Bury) {

          res.append(specieName + " BURY intervals: ");

          // have to bury something, do the intervals first
          TreeSet<Interval> intervals = new TreeSet<Interval>(new IntervalComparator());
          Double currentLow = 0D;
          for (Specie sp : this.surfaceStorage.values()) {
            Double currentHigh = currentLow + sp.getAccumulatorValue()
                / totalActiveSize.doubleValue();
            intervals.add(new Interval(sp.getName(), currentLow, currentHigh));
            currentLow = currentHigh;
          }
          Double rnd = this.random.nextDouble();
          Interval[] intervalsArray = new Interval[intervals.size()];
          String spName = getHit(rnd, intervals.toArray(intervalsArray));

          // logging
          res.append("rnd " + rnd + " ");
          for (Interval i : intervals) {
            res.append(i.getName() + "(" + i.getLowBound() + "," + i.getHighBound()
                + "), selected: " + spName + " ");
          }

          // bury specie
          this.surfaceDecrementSpecie(spName);
          if (this.buriedStorage.containsKey(spName)) {
            this.buriedStorage.get(spName).incrementAccumulatorValue(1L);
          }
          else {
            if (spName.length() > 1) {
              this.buriedStorage.put(spName, new Molecule(spName, 1L));
            }
            else {
              this.buriedStorage.put(spName, new Atom(spName, 1L));
            }
          }

          // add the specie
          if (this.surfaceStorage.containsKey(specieName)) {
            this.surfaceStorage.get(specieName).incrementAccumulatorValue(1L);
          }
          else {
            Specie sp = null;
            if (specieName.length() > 1) {
              sp = new Molecule(specieName, 1L);
            }
            else {
              sp = new Atom(specieName, 1L);
            }
            this.surfaceStorage.put(specieName, sp);
          }
        }
        else {
          // add the specie
          if (this.surfaceStorage.containsKey(specieName)) {
            this.surfaceStorage.get(specieName).incrementAccumulatorValue(1L);
          }
          else {
            Specie sp = null;
            if (specieName.length() > 1) {
              sp = new Molecule(specieName, 1L);
            }
            else {
              sp = new Atom(specieName, 1L);
            }
            this.surfaceStorage.put(specieName, sp);
          }
        }
      }

      // logging
      res.append(",RESULT active:" + this.surfaceTotalSize() + ",buried:" + this.buriedTotalSize());
    }

    return res.toString();
  }

  /**
   * Reports whether or not this buried population contains such a specie.
   * 
   * @param name the specie name to check.
   * @return true if contains.
   */
  public boolean buriedContains(String name) {
    return this.buriedStorage.containsKey(name);
  }

  /**
   * Provides the iterator over all buried species.
   * 
   * @return iterator over the buried species.
   */
  public Iterator buriedGetIterator() {
    return this.buriedStorage.values().iterator();
  }

  /**
   * Reports size of buried population.
   * 
   * @return the amount of different species within the buried population.
   */
  public Integer buriedSpeciesNum() {
    return this.buriedStorage.size();
  }

  /**
   * Report in total number of species in active population.
   * 
   * @return buried population volume.
   */
  public Long buriedTotalSize() {
    Long res = 0L;
    for (Specie sp : this.buriedStorage.values()) {
      res += sp.getAccumulatorValue();
    }
    return res;
  }

  /**
   * Reports interval selected by provided random value.
   * 
   * @param rnd the random value.
   * @param intervals the array of intervals.
   * @return the specie that correspond to selected interval.
   */
  private String getHit(Double rnd, Interval[] intervals) {
    int lowIdx = 0;
    int highIdx = intervals.length;
    int curIdx = (highIdx - lowIdx) / 2;
    while (!intervals[curIdx].within(rnd)) {
      if (rnd < intervals[curIdx].getLowBound()) {
        highIdx = curIdx;
      }
      else {
        lowIdx = curIdx;
      }
      curIdx = lowIdx + (highIdx - lowIdx) / 2;
    }
    return intervals[curIdx].getName();
  }

  /**
   * Generates text message that used to log the Chemistry process.
   * 
   * @return the message.
   */
  public StringBuffer getLogMessage() {
    StringBuffer reply = new StringBuffer(1500);
    reply.append("surface: ");
    for (Specie sp : this.surfaceStorage.values()) {
      reply.append(sp.getName() + " " + sp.getAccumulatorValue() + " ");
    }
    reply.append("buried: ");
    for (Specie sp : this.buriedStorage.values()) {
      reply.append(sp.getName() + " " + sp.getAccumulatorValue() + " ");
    }
    return (reply);
  }

  /**
   * Reports whether or not this population contains such a specie.
   * 
   * @param specieName specie to check.
   * @return true if contains.
   */
  public boolean surfaceContains(String specieName) {
    return this.surfaceStorage.containsKey(specieName);
  }

  /**
   * Decrements the accumulator value of the certain specie by the certain amount.
   * 
   * @param specieName specie specified.
   * @throws PopulationException if specie is not found.
   */
  public void surfaceDecrementSpecie(String specieName) throws PopulationException {
    if (this.surfaceStorage.containsKey(specieName)) {
      Specie sp = this.surfaceStorage.get(specieName);
      sp.decrementAccumulatorValue(1L);
      if (sp.getAccumulatorValue() <= 0) {
        this.surfaceStorage.remove(sp.getName());
      }
    }
    else {
      throw new PopulationException("specie " + specieName
          + "not found in the population while decrementing.");
    }
  }

  /**
   * Provides iterator over the species.
   * 
   * @return the iterator.
   */
  public Iterator<Specie> surfaceGetIterator() {
    return this.surfaceStorage.values().iterator();
  }

  /**
   * Reports specified specie.
   * 
   * @param specieName specified specie.
   * @return specie if found within the storage or null.
   */
  public Specie surfaceGetSpecie(String specieName) {
    if (this.surfaceStorage.containsKey(specieName)) {
      return this.surfaceStorage.get(specieName);
    }
    return null;
  }

  /**
   * Removes specified specie from the storage.
   * 
   * @param specieName specie name.
   */
  public void surfaceRemoveSpecie(String specieName) {
    if (this.surfaceStorage.containsKey(specieName)) {
      this.surfaceStorage.remove(specieName);
    }
  }

  /**
   * Reports size of surface population.
   * 
   * @return the amount of different species within the surface population.
   */
  public Integer surfaceSpeciesNum() {
    return this.surfaceStorage.size();
  }

  /**
   * Report in total number of species in active population.
   * 
   * @return active population volume.
   */
  public Long surfaceTotalSize() {
    Long res = 0L;
    for (Specie sp : this.surfaceStorage.values()) {
      res += sp.getAccumulatorValue();
    }
    return res;
  }

  /**
   * Switches off the bury mechanism.
   * 
   */
  public void switchBuryOff() {
    this.buryOff = true;
  }

  /**
   * Switches on the bury mechanism.
   * 
   */
  public void switchBuryOn() {
    this.buryOff = false;
  }

  /**
   * Reports if the population contains the specie, buried or not.
   * 
   * @param st the queried specie name.
   * @return true if contains, false otherwise.
   */
  public boolean totalContains(String st) {
    if (this.surfaceStorage.containsKey(st) || this.buriedStorage.containsKey(st)) {
      return true;
    }
    return false;
  }

  /**
   * Report in total number of species in this population.
   * 
   * @return total population number.
   */
  public Long totalTotalSize() {
    Long res = 0L;
    for (Specie sp : this.surfaceStorage.values()) {
      res += sp.getAccumulatorValue();
    }
    for (Specie sp : this.buriedStorage.values()) {
      res += sp.getAccumulatorValue();
    }
    return res;
  }

  /**
   * Reports all the species names.
   * 
   * @return the species names within the population.
   */
  public Set<String> totalGetSpeciesNames() {
    TreeSet<String> names = new TreeSet<String>();
    for (String s : this.surfaceStorage.keySet()) {
      if (!names.contains(s)) {
        names.add(s);
      }
    }
    for (String s : this.buriedStorage.keySet()) {
      if (!names.contains(s)) {
        names.add(s);
      }
    }
    return names;
  }

  /**
   * Reports total amount of specie (buried + active) sitting on the grain.
   * 
   * @param st the specie name.
   * @return the queried amount.
   */
  public Long totalSpecieVolume(String st) {
    Long sum1 = 0L;
    Long sum2 = 0L;
    if (this.surfaceStorage.containsKey(st)) {
      sum1 = this.surfaceStorage.get(st).getAccumulatorValue();
    }
    if (this.buriedStorage.containsKey(st)) {
      sum2 = this.buriedStorage.get(st).getAccumulatorValue();
    }
    return sum1 + sum2;
  }
}
